import {
    matchesSelectorAndParentsTo, addEvent, removeEvent, addUserSelectStyles, getTouchIdentifier,
    removeUserSelectStyles
} from './utils/domFns';
import { createCoreData, getControlPosition, snapToGrid } from './utils/positionFns';
import log from './utils/log';

import type { EventHandler, MouseTouchEvent } from './utils/types';
import { batch, children, createComputed, createSignal, onCleanup } from 'solid-js';

// Simple abstraction for dragging events names.
const eventsFor = {
    touch: {
        start: 'touchstart',
        move: 'touchmove',
        stop: 'touchend'
    },
    mouse: {
        start: 'mousedown',
        move: 'mousemove',
        stop: 'mouseup'
    }
};

// Default to mouse events.
let dragEventFor = eventsFor.mouse;

export type DraggableData = {
    node: HTMLElement,
    x: number, y: number,
    deltaX: number, deltaY: number,
    lastX: number, lastY: number,
};

export type DraggableEventHandler = (e: MouseEvent, data: DraggableData) => void | false;

export type ControlPosition = { x: number, y: number };
export type PositionOffsetControlPosition = { x: number | string, y: number | string };

export type DraggableCoreDefaultProps = {
    allowAnyClick?: boolean,
    disabled?: boolean,
    enableUserSelectHack?: boolean,
    onStart?: DraggableEventHandler,
    onDrag?: DraggableEventHandler,
    onStop?: DraggableEventHandler,
    onMouseDown?: (e: MouseEvent) => void,
    scale?: number,
};

export type DraggableCoreProps = {
    cancel?: string,
    children: any,
    offsetParent?: HTMLElement,
    grid?: [number, number],
    handle?: string,
    transform?: string,
    classList?: any,
    style?: any,
    ref?: any
} & DraggableCoreDefaultProps;

//
// Define <DraggableCore>.
//
// <DraggableCore> is for advanced usage of <Draggable>. It maintains minimal internal state so it can
// work well with libraries that require more control over the element.
//
export default function DraggableCore (props: DraggableCoreProps) {
    const [dragging, setDragging] = createSignal(false);
    const [lastX, setLastX] = createSignal(NaN);
    const [lastY, setLastY] = createSignal(NaN);
    const [touchIdentifier, setTouchIdentifier] = createSignal(null);

    // // React Strict Mode compatibility: if `nodeRef` is passed, we will use it instead of trying to find
    // // the underlying DOM node ourselves. See the README for more information.
    const findDOMNode = () : Element => {
        return targetEl;
    }

    const handleDragStart = (e) : EventHandler<MouseTouchEvent> => {
        // Make it possible to attach event handlers on top of this one.
        props.onMouseDown && props.onMouseDown(e);
        // Only accept left-clicks.
        if (!props.allowAnyClick && typeof e.button === 'number' && e.button !== 0) return;
        
        // Get nodes. Be sure to grab relative document (could be iframed)
        const thisNode = findDOMNode();
        if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) {
            throw new Error('<DraggableCore> not mounted on DragStart!');
        }
        const { ownerDocument } = thisNode;

        // Short circuit if handle or cancel prop was provided and selector doesn't match.
        if (props.disabled ||
            (!(e.target instanceof ownerDocument.defaultView.Node)) ||
            (props.handle && !matchesSelectorAndParentsTo(e.target, props.handle, thisNode)) ||
            (props.cancel && matchesSelectorAndParentsTo(e.target, props.cancel, thisNode))) {
            return;
        }

        // Prevent scrolling on mobile devices, like ipad/iphone.
        // Important that this is after handle/cancel.
        if (e.type === 'touchstart') e.preventDefault();

        // Set touch identifier in component state if this is a touch event. This allows us to
        // distinguish between individual touches on multitouch screens by identifying which
        // touchpoint was set to this element.
        const touchIdentifier = getTouchIdentifier(e);
        setTouchIdentifier(touchIdentifier);

        // Get the current drag point from the event. This is used as the offset.
        const position = getControlPosition(e, touchIdentifier, props, thisNode as HTMLElement);
        if (position == null) return; // not possible but satisfies flow
        const { x, y } = position;
        // Create an event object with all the data parents need to make a decision here.
        const coreEvent = createCoreData({lastX, lastY}, thisNode as HTMLElement, x, y);
        log('DraggableCore: handleDragStart: %j', coreEvent);

        // Call event handler. If it returns explicit false, cancel.
        log('calling', props.onStart);
        const shouldUpdate = props.onStart && props.onStart(e, coreEvent);
        if (shouldUpdate === false) return;

        // Add a style to the body to disable user-select. This prevents text from
        // being selected all over the page.
        if (props.enableUserSelectHack) addUserSelectStyles(ownerDocument);

        // Initiate dragging. Set the current x and y as offsets
        // so we know how much we've moved during the drag. This allows us
        // to drag elements around even if they have been moved, without issue.
        batch(() => {
            setDragging(true);
            setLastX(x);
            setLastY(y);
        })

        // Add events to the document directly so we catch when the user's mouse/touch moves outside of
        // this element. We use different events depending on whether or not we have detected that this
        // is a touch-capable device.
        addEvent(ownerDocument, dragEventFor.move, handleDrag);
        addEvent(ownerDocument, dragEventFor.stop, handleDragStop);
    };

    const handleDrag: EventHandler<MouseTouchEvent> = (e) => {

        // Get the current drag point from the event. This is used as the offset.
        const position = getControlPosition(e, touchIdentifier(), props, findDOMNode() as HTMLElement);
        if (position == null) return;
        let { x, y } = position;
        
        // Snap to grid if prop has been provided
        if (Array.isArray(props.grid)) {
            let deltaX = x - lastX(), deltaY = y - lastY();
            [deltaX, deltaY] = snapToGrid(props.grid, deltaX, deltaY);
            if (!deltaX && !deltaY) return; // skip useless drag
            x = lastX() + deltaX, y = lastY() + deltaY;
        }

        const coreEvent = createCoreData({lastX, lastY}, findDOMNode() as HTMLElement, x, y);

        log('DraggableCore: handleDrag: %j', coreEvent);

        // Call event handler. If it returns explicit false, trigger end.
        const shouldUpdate = props.onDrag && props.onDrag(e, coreEvent);
        if (shouldUpdate === false) {
            try {
                // $FlowIgnore
                handleDragStop(new MouseEvent('mouseup'));
            } catch (err) {
                // Old browsers
                const event = ((document.createEvent('MouseEvents') as any) as MouseTouchEvent);
                // I see why this insanity was deprecated
                // $FlowIgnore
                event.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
                handleDragStop(event);
            }
            return;
        }

        batch(() => {
            setLastX(x);
            setLastY(y);
        })
    };

    const handleDragStop = (e): EventHandler<MouseTouchEvent> => {
        if (!dragging()) return;

        const position = getControlPosition(e, touchIdentifier(), props, findDOMNode() as HTMLElement);
        if (position == null) return;
        let { x, y } = position;

        // Snap to grid if prop has been provided
        if (Array.isArray(props.grid)) {
            let deltaX = x - lastX() || 0;
            let deltaY = y - lastY() || 0;
            [deltaX, deltaY] = snapToGrid(props.grid, deltaX, deltaY);
            x = lastX() + deltaX, y = lastY() + deltaY;
        }

        const coreEvent = createCoreData({lastX, lastY}, findDOMNode() as HTMLElement, x, y);

        // Call event handler
        const shouldContinue = props.onStop && props.onStop(e, coreEvent);
        if (shouldContinue === false) return;

        const thisNode = findDOMNode();
        if (thisNode) {
            // Remove user-select hack
            if (props.enableUserSelectHack) removeUserSelectStyles(thisNode.ownerDocument);
        }

        log('DraggableCore: handleDragStop: %j', coreEvent);

        // Reset the el.
        batch(() => {
            setDragging(false);
            setLastX(NaN);
            setLastY(NaN);
        })

        if (thisNode) {
            // Remove event handlers
            log('DraggableCore: Removing handlers');
            removeEvent(thisNode.ownerDocument, dragEventFor.move, handleDrag);
            removeEvent(thisNode.ownerDocument, dragEventFor.stop, handleDragStop);
        }
    };

    const onMouseDown = (e) : EventHandler<MouseTouchEvent> => {
        dragEventFor = eventsFor.mouse; // on touchscreen laptops we could switch back to mouse
    
        return handleDragStart(e);
    };
    
    const onMouseUp = (e) => {
        dragEventFor = eventsFor.mouse;
    
        setTimeout(() => {
            handleDragStop(e);
        })
    };
    
      // Same as onMouseDown (start drag), but now consider this a touch device.
    const onTouchStart = (e) : EventHandler<MouseTouchEvent> => {
        // We're on a touch device now, so change the event handlers
        dragEventFor = eventsFor.touch;
    
        return handleDragStart(e);
      };
    
    const onTouchEnd = (e) : EventHandler<MouseTouchEvent> => {
        // We're on a touch device now, so change the event handlers
        dragEventFor = eventsFor.touch;
    
        return handleDragStop(e);
    };

    let targetEl: Element = null;
    const addEventListeners = (el: Element) => {
        targetEl = el;
        addEvent(el, eventsFor.touch.start, onTouchStart, { passive: false });
        el.addEventListener('mousedown', onMouseDown, { passive: false });
        el.addEventListener('mouseup', onMouseUp, { passive: false });
        el.addEventListener('touchend', onTouchEnd, { passive: false });
    }

    onCleanup(() => {
        if (targetEl) {
            removeEvent(targetEl, 'mousedown', onMouseDown);
            removeEvent(targetEl, 'mouseup', onMouseUp);
            removeEvent(targetEl, 'touchend', onTouchEnd);

            const thisNode = findDOMNode();
            const { ownerDocument } = thisNode;
            removeEvent(ownerDocument, eventsFor.mouse.move, handleDrag);
            removeEvent(ownerDocument, eventsFor.touch.move, handleDrag);
            removeEvent(ownerDocument, eventsFor.mouse.stop, handleDragStop);
            removeEvent(ownerDocument, eventsFor.touch.stop, handleDragStop);
            removeEvent(targetEl, eventsFor.touch.start, onTouchStart, { passive: false });
            if (props.enableUserSelectHack) removeUserSelectStyles(ownerDocument);
        }
    })

    props.ref && props.ref(() =>{
        return {
            target: targetEl
        }
    })

    const c = children(() => props.children);
    createComputed(() => {
        const length = c.toArray().length;
        if (length > 1 || length === 0) {
            console.error('Solid-Draggable need only one child Element');
            return;
        }
        
        c.toArray().forEach((item: Element) => {
            addEventListeners(item);
        })
    })

    createComputed(() => {
        if (!targetEl) {
            return;
        }
        if (props.transform) {
            (targetEl as any).transform = props.transform;
        }
        const el = (targetEl as HTMLElement);
        for (let key in props.style) {
            el.style[key] = props.style[key];
        }
        for (let key in props.classList) {
            if (props.classList[key]) {
                el.classList.add(key);
            } else {
                el.classList.remove(key);
            }
        }
    })
    return <>{c()}</>;
}